/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.dao.impl;

import es.silenus.detecta.dao.Page;

import org.hibernate.*;

import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;

import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;


/**
 * Page implementation.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @param <T> the templated element.
 *
 * @since 02-abr-2009 1:03:14
 */
public class PageImpl<T> extends HibernateDaoSupport implements Page<T> {
	/**
	 * The projection hack.
	 */
	private boolean projectionHack;

	/**
	 * The start result.
	 */
	private int start;

	/**
	 * The limit result or page size.
	 */
	private int limit;

	/**
	 * The total number of results.
	 */
	private int total;

	/**
	 * The order criterions.
	 */
	private final List<Order> orders;

	/**
	 * The results.
	 */
	private List<T> results;

	/**
	 * The detached criteria.
	 */
	private DetachedCriteria criteria;

/**
   * Constructor.
   */
	public PageImpl() {
		start = 0;
		limit = 100;
		total = 0;
		projectionHack = false;
		orders = new ArrayList<Order>();
	}

/**
   * Constructor.
   *
   * @param sessionFactory the session factory.
   */
	public PageImpl(final SessionFactory sessionFactory) {
		start = 0;
		limit = 100;
		total = 0;
		projectionHack = false;
		orders = new ArrayList<Order>();
		setSessionFactory(sessionFactory);
	}

	/**
	 * Sets the projection hack flag.
	 *
	 * @param projectionHack the projection hack flag.
	 */
	public void setProjectionHack(boolean projectionHack) {
		this.projectionHack = projectionHack;
	}

	/**
	 * Retrieves the start offset.
	 *
	 * @return the start offset.
	 */
	public int getStart() {
		return start;
	}

	/**
	 * Sets the start offset.
	 *
	 * @param start the start offset.
	 */
	public void setStart(int start) {
		this.start = start;
	}

	/**
	 * Retrieves the limit or page size.
	 *
	 * @return the limit or page size.
	 */
	public int getLimit() {
		return limit;
	}

	/**
	 * Sets the limit or page size.
	 *
	 * @param limit the limit or page size.
	 */
	public void setLimit(int limit) {
		this.limit = limit;
	}

	/**
	 * Adds a sort field criteria.
	 *
	 * @param field the field.
	 * @param direction the sort direction: ASC / DESC.
	 */
	public void addSort(String field, String direction) {
		if("asc".equalsIgnoreCase(direction)) {
			orders.add(Order.asc(map(field)));
		} else {
			orders.add(Order.desc(map(field)));
		}
	}

	/**
	 * Retrieves the results of this page.
	 *
	 * @return the results of this page.
	 */
	public List<T> getResults() {
		return results;
	}

	/**
	 * Retrieves the total results.
	 *
	 * @return the total results.
	 */
	public int getTotal() {
		return total;
	}

	/**
	 * Sets the criteria.
	 *
	 * @param criteria the criteria.
	 */
	public void setCriteria(DetachedCriteria criteria) {
		this.criteria = criteria;
	}

	/**
	 * Maps a field name to a criterion property. Override this method when needed.
	 *
	 * @param field the field.
	 *
	 * @return the mapped property.
	 */
	public String map(final String field) {
		return field;
	}

	/**
	 * Performs pagination. It is implementation specific.
	 */
	@SuppressWarnings({"unchecked"})
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public void paginate() {
		if(projectionHack) {
			paginateWithCriteria();
		} else {
			paginateWithSroll();
		}
	}

	/**
	 * Performs pagination. It is implementation specific.
	 */
	@SuppressWarnings({"unchecked"})
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public void paginateWithSroll() {
		Session session = null;

		try {
			session = getSession();

			for(Order o : orders) {
				criteria.addOrder(o);
			}

			// There is a bug still open in Hibernate:
			// 	http://opensource.atlassian.com/projects/hibernate/browse/HHH-2480

			// theCriteria.setFirstResult(pageNumber * pageSize).setMaxResults(pageSize).list();

			// Build real criteria
			final Criteria theCriteria = criteria.getExecutableCriteria(session);

			// Count results
			final ScrollableResults scrollableResults = theCriteria.scroll(ScrollMode.SCROLL_SENSITIVE);

			// Move to the last one to retrieve the total count
			scrollableResults.last();

			total = scrollableResults.getRowNumber() + 1;

			// Rewind to fetch data
			scrollableResults.first();

			results = new ArrayList<T>(limit);

			if(scrollableResults.scroll(start)) {
				results.add((T)scrollableResults.get(0));

				for(int i = 1; i < limit; i++) {
					if(scrollableResults.next()) {
						results.add((T)scrollableResults.get(0));
					}
				}
			}
		} finally {
			releaseSession(session);
		}
	}

	/**
	 * Performs pagination. It is implementation specific.
	 */
	@SuppressWarnings({"unchecked"})
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public void paginateWithCriteria() {
		Session session = null;

		try {
			session = getSession();


			// Build real criteria
			final Criteria theCriteria = criteria.getExecutableCriteria(session);

			// See: http://forum.hibernate.org/viewtopic.php?t=939039

			// See: http://raibledesigns.com/wiki/Wiki.jsp?page=HibernatePagination
			total = (Integer)theCriteria.setProjection(Projections.rowCount()).uniqueResult();

			for(Order o : orders) {
				criteria.addOrder(o);
			}

			theCriteria.setProjection(null);
			theCriteria.setResultTransformer(DetachedCriteria.DISTINCT_ROOT_ENTITY);

			results = theCriteria.setFirstResult(start).setMaxResults(limit).list();
		} finally {
			releaseSession(session);
		}
	}
}
